/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.catalina.core;

import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Set;

import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

import org.apache.catalina.Globals;
import org.apache.catalina.security.SecurityUtil;

/**
 * Facade object which masks the internal <code>ApplicationContext</code> object
 * from the web application.
 * 
 * @author Remy Maucherat
 * @author Jean-Francois Arcand
 * @version $Id: ApplicationContextFacade.java 939336 2010-04-29 15:00:41Z
 *          kkolinko $
 */

public final class ApplicationContextFacade implements ServletContext {

	// ---------------------------------------------------------- Attributes
	/**
	 * Cache Class object used for reflection.
	 */
	private HashMap classCache;

	/**
	 * Cache method object.
	 */
	private HashMap objectCache;

	// ----------------------------------------------------------- Constructors

	/**
	 * Construct a new instance of this class, associated with the specified
	 * Context instance.
	 * 
	 * @param context
	 *            The associated Context instance
	 */
	public ApplicationContextFacade(ApplicationContext context) {
		super();
		this.context = context;

		classCache = new HashMap();
		objectCache = new HashMap();
		initClassCache();
	}

	private void initClassCache() {
		Class[] clazz = new Class[] { String.class };
		classCache.put("getContext", clazz);
		classCache.put("getMimeType", clazz);
		classCache.put("getResourcePaths", clazz);
		classCache.put("getResource", clazz);
		classCache.put("getResourceAsStream", clazz);
		classCache.put("getRequestDispatcher", clazz);
		classCache.put("getNamedDispatcher", clazz);
		classCache.put("getServlet", clazz);
		classCache.put("getInitParameter", clazz);
		classCache.put("setAttribute",
				new Class[] { String.class, Object.class });
		classCache.put("removeAttribute", clazz);
		classCache.put("getRealPath", clazz);
		classCache.put("getAttribute", clazz);
		classCache.put("log", clazz);
	}

	// ----------------------------------------------------- Instance Variables

	/**
	 * Wrapped application context.
	 */
	private ApplicationContext context = null;

	// ------------------------------------------------- ServletContext Methods

	public ServletContext getContext(String uripath) {
		ServletContext theContext = null;
		if (SecurityUtil.isPackageProtectionEnabled()) {
			theContext = (ServletContext) doPrivileged("getContext",
					new Object[] { uripath });
		} else {
			theContext = context.getContext(uripath);
		}
		if ((theContext != null) && (theContext instanceof ApplicationContext)) {
			theContext = ((ApplicationContext) theContext).getFacade();
		}
		return (theContext);
	}

	public int getMajorVersion() {
		return context.getMajorVersion();
	}

	public int getMinorVersion() {
		return context.getMinorVersion();
	}

	public String getMimeType(String file) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (String) doPrivileged("getMimeType", new Object[] { file });
		} else {
			return context.getMimeType(file);
		}
	}

	public Set getResourcePaths(String path) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (Set) doPrivileged("getResourcePaths", new Object[] { path });
		} else {
			return context.getResourcePaths(path);
		}
	}

	public URL getResource(String path) throws MalformedURLException {
		if (Globals.IS_SECURITY_ENABLED) {
			try {
				return (URL) invokeMethod(context, "getResource",
						new Object[] { path });
			} catch (Throwable t) {
				if (t instanceof MalformedURLException) {
					throw (MalformedURLException) t;
				}
				return null;
			}
		} else {
			return context.getResource(path);
		}
	}

	public InputStream getResourceAsStream(String path) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (InputStream) doPrivileged("getResourceAsStream",
					new Object[] { path });
		} else {
			return context.getResourceAsStream(path);
		}
	}

	public RequestDispatcher getRequestDispatcher(final String path) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (RequestDispatcher) doPrivileged("getRequestDispatcher",
					new Object[] { path });
		} else {
			return context.getRequestDispatcher(path);
		}
	}

	public RequestDispatcher getNamedDispatcher(String name) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (RequestDispatcher) doPrivileged("getNamedDispatcher",
					new Object[] { name });
		} else {
			return context.getNamedDispatcher(name);
		}
	}

	public Servlet getServlet(String name) throws ServletException {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			try {
				return (Servlet) invokeMethod(context, "getServlet",
						new Object[] { name });
			} catch (Throwable t) {
				if (t instanceof ServletException) {
					throw (ServletException) t;
				}
				return null;
			}
		} else {
			return context.getServlet(name);
		}
	}

	public Enumeration getServlets() {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (Enumeration) doPrivileged("getServlets", null);
		} else {
			return context.getServlets();
		}
	}

	public Enumeration getServletNames() {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (Enumeration) doPrivileged("getServletNames", null);
		} else {
			return context.getServletNames();
		}
	}

	public void log(String msg) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			doPrivileged("log", new Object[] { msg });
		} else {
			context.log(msg);
		}
	}

	public void log(Exception exception, String msg) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			doPrivileged("log", new Class[] { Exception.class, String.class },
					new Object[] { exception, msg });
		} else {
			context.log(exception, msg);
		}
	}

	public void log(String message, Throwable throwable) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			doPrivileged("log", new Class[] { String.class, Throwable.class },
					new Object[] { message, throwable });
		} else {
			context.log(message, throwable);
		}
	}

	public String getRealPath(String path) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (String) doPrivileged("getRealPath", new Object[] { path });
		} else {
			return context.getRealPath(path);
		}
	}

	public String getServerInfo() {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (String) doPrivileged("getServerInfo", null);
		} else {
			return context.getServerInfo();
		}
	}

	public String getInitParameter(String name) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (String) doPrivileged("getInitParameter",
					new Object[] { name });
		} else {
			return context.getInitParameter(name);
		}
	}

	public Enumeration getInitParameterNames() {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (Enumeration) doPrivileged("getInitParameterNames", null);
		} else {
			return context.getInitParameterNames();
		}
	}

	public Object getAttribute(String name) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return doPrivileged("getAttribute", new Object[] { name });
		} else {
			return context.getAttribute(name);
		}
	}

	public Enumeration getAttributeNames() {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (Enumeration) doPrivileged("getAttributeNames", null);
		} else {
			return context.getAttributeNames();
		}
	}

	public void setAttribute(String name, Object object) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			doPrivileged("setAttribute", new Object[] { name, object });
		} else {
			context.setAttribute(name, object);
		}
	}

	public void removeAttribute(String name) {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			doPrivileged("removeAttribute", new Object[] { name });
		} else {
			context.removeAttribute(name);
		}
	}

	public String getServletContextName() {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (String) doPrivileged("getServletContextName", null);
		} else {
			return context.getServletContextName();
		}
	}

	public String getContextPath() {
		if (SecurityUtil.isPackageProtectionEnabled()) {
			return (String) doPrivileged("getContextPath", null);
		} else {
			return context.getContextPath();
		}
	}

	/**
	 * Use reflection to invoke the requested method. Cache the method object to
	 * speed up the process
	 * 
	 * @param methodName
	 *            The method to call.
	 * @param params
	 *            The arguments passed to the called method.
	 */
	private Object doPrivileged(final String methodName, final Object[] params) {
		try {
			return invokeMethod(context, methodName, params);
		} catch (Throwable t) {
			throw new RuntimeException(t.getMessage(), t);
		}
	}

	/**
	 * Use reflection to invoke the requested method. Cache the method object to
	 * speed up the process
	 * 
	 * @param appContext
	 *            The AppliationContext object on which the method will be
	 *            invoked
	 * @param methodName
	 *            The method to call.
	 * @param params
	 *            The arguments passed to the called method.
	 */
	private Object invokeMethod(ApplicationContext appContext,
			final String methodName, Object[] params) throws Throwable {

		try {
			Method method = (Method) objectCache.get(methodName);
			if (method == null) {
				method = appContext.getClass().getMethod(methodName,
						(Class[]) classCache.get(methodName));
				objectCache.put(methodName, method);
			}

			return executeMethod(method, appContext, params);
		} catch (Exception ex) {
			handleException(ex, methodName);
			return null;
		} finally {
			params = null;
		}
	}

	/**
	 * Use reflection to invoke the requested method. Cache the method object to
	 * speed up the process
	 * 
	 * @param methodName
	 *            The method to invoke.
	 * @param clazz
	 *            The class where the method is.
	 * @param params
	 *            The arguments passed to the called method.
	 */
	private Object doPrivileged(final String methodName, final Class[] clazz,
			Object[] params) {

		try {
			Method method = context.getClass().getMethod(methodName,
					(Class[]) clazz);
			return executeMethod(method, context, params);
		} catch (Exception ex) {
			try {
				handleException(ex, methodName);
			} catch (Throwable t) {
				throw new RuntimeException(t.getMessage());
			}
			return null;
		} finally {
			params = null;
		}
	}

	/**
	 * Executes the method of the specified <code>ApplicationContext</code>
	 * 
	 * @param method
	 *            The method object to be invoked.
	 * @param context
	 *            The AppliationContext object on which the method will be
	 *            invoked
	 * @param params
	 *            The arguments passed to the called method.
	 */
	private Object executeMethod(final Method method,
			final ApplicationContext context, final Object[] params)
			throws PrivilegedActionException, IllegalAccessException,
			InvocationTargetException {

		if (SecurityUtil.isPackageProtectionEnabled()) {
			return AccessController
					.doPrivileged(new PrivilegedExceptionAction() {
						public Object run() throws IllegalAccessException,
								InvocationTargetException {
							return method.invoke(context, params);
						}
					});
		} else {
			return method.invoke(context, params);
		}
	}

	/**
	 * 
	 * Throw the real exception.
	 * 
	 * @param ex
	 *            The current exception
	 */
	private void handleException(Exception ex, String methodName)
			throws Throwable {

		Throwable realException;

		if (ex instanceof PrivilegedActionException) {
			ex = ((PrivilegedActionException) ex).getException();
		}

		if (ex instanceof InvocationTargetException) {
			realException = ((InvocationTargetException) ex)
					.getTargetException();
		} else {
			realException = ex;
		}

		throw realException;
	}
}
